#!/usr/bin/python

import os
import re
import sys
import time
import codecs

import xmpp

import dictionary
from templates import *

# SYSTEM SETTINGS SECTION

CONTACTS_DIR = 'contacts'
LOGS_DIR = 'logs'
CONFIG_FILE = 'config.txt'
COMMAND_PREFIX = '/'
POLL_INTERVAL = 0.1
SUBSCRIBE_INTERVAL = 0.3

# END OF SYSTEM SETTINGS


class DictBot:

	def __init__(self, domain, username, password, url, port):
		"Setup connection to server"
#		self.client = xmpp.Client(domain)
		self.client = xmpp.Client(domain, debug=[])
		self.client.connect(server = (url, port))
		self.client.auth(username, password)
		self.client.sendInitPresence()
		self.roster = self.client.getRoster()
		self.client.RegisterHandler('message', self.handle_Message)
		self.client.RegisterHandler('presence', self.handle_Presence)
		self.JID = '%s@%s' % (username, domain)


	def process_Events(self):
		self.client.Process()
		# In order to avoid rejection from server, wait for a while after each poll
		time.sleep(POLL_INTERVAL)


	def handle_Presence(self, dispatcher, presence):
		"Accepts user subscriptions or forward it to the bot with least contacts"
		global contactCounts
		sender = presence.getFrom()
		senderJID = '%s@%s' % (sender.getNode(), sender.getDomain())

		type = presence.getType()
		if type == 'subscribe':

			# Check if the user has already subscribed in one of the bot accounts
			for bot in botList:
				# A subscribe request is not sent from the server, unless the subscriber has not been subscribed,
				# but even if it is a new user, an item will be found in the subscribee's roster.
				if senderJID in bot.roster.getItems() and bot.roster.getSubscription(senderJID) not in ('none', 'remove'):
					dispatcher.send(xmpp.Message(sender, ALREADY_ADDED_MSG % bot.JID))
					self.roster.Unauthorize(senderJID)
					self.roster.Unsubscribe(senderJID)
					self.roster.delItem(senderJID)		# Also delete in the roster
					return

			print 'EVENT (%s): subscription request by %s' % (self.JID, sender)
			print 'REPORT (%s): least populated account is %s' % (self.JID, get_LeastPopulatedAccount())
			minNodeJID = get_LeastPopulatedAccount()
			if minNodeJID == self.JID:
				# Just accept the request and welcome
				self.roster.Authorize(sender)
				# We might be updating the counts twice, if all authorizations send back "subscribed"
				#contactCounts[self.JID] += 1
				#dispatcher.send(xmpp.Message(sender, WELCOME_MSG))
				print 'EVENT (%s): authorized to %s' % (self.JID, senderJID)
			else:
				# Forward the subscribe request to the least populated bot
				senderJID = '%s@%s' % (sender.getNode(), sender.getDomain())
				command = '/invite ' + senderJID
				#dispatcher.send(xmpp.Message(minNodeJID, command))
				dispatcher.send(xmpp.Message(sender, FORWARDED_MSG % minNodeJID ))
				self.roster.Unauthorize(senderJID)
				self.roster.delItem(senderJID)
				time.sleep(SUBSCRIBE_INTERVAL)

		elif type == 'subscribed':
			# After successful subscription, update the counts and welcome
			contactCounts[self.JID] += 1
			dispatcher.send(xmpp.Message(sender, WELCOME_MSG))

			# Update status message
			presence = xmpp.protocol.Presence()
			presence.setStatus('Auto-update: %d users now (type /help for help)' % (contactCounts[self.JID]))
			dispatcher.send(presence)
			print 'EVENT (%s): subscribed to %s' % (self.JID, senderJID)
			print 'UPDATE (%s): contact counts %s' % (self.JID, str(contactCounts))


	def handle_Message(self, dispatcher, message):
		"Processes commands and dictionary lookup requests"
		request = message.getBody()
		if request:
			sender = message.getFrom()
#			senderJID = '%s@%s' % (sender.getNode(), sender.getDomain())
			try:
				print '\t<%s>: %s  (%s)' % (self.JID, request, sender)
			except StandardError:
				pass
			request = request.strip()
			if request.startswith(COMMAND_PREFIX) and len(request) > 1:
				self.process_Command(dispatcher, sender, request[1:])
			else:
				self.process_Lookup(dispatcher, sender, request)


	def process_Lookup(self, dispatcher, sender, lookup):
		if lookup.startswith('Sorry,'): # Echo from unsubscribed users
			return
		lookup = lookup.lower()
		if dictionary.mainDict.has_key(lookup):
			reply = dictionary.mainDict[lookup]
		elif dictionary.addedDict.has_key(lookup):
			reply = LOOKUP_IN_ADDS_MSG % (lookup, dictionary.addedDict[lookup])
		else:
			wordnetResult = dictionary.lookup_WordNet(lookup)
			if wordnetResult:
				reply = LOOKUP_IN_WORDNET_MSG % (lookup, wordnetResult)
			else:
				reply = LOOKUP_NOT_FOUND_MSG
		dispatcher.send(xmpp.Message(sender, reply))


	def process_Command(self, dispatcher, sender, command):
		senderJID = '%s@%s' % (sender.getNode(), sender.getDomain())
		command = command.split()
		cmd = command[0]

# USER COMMANDS SECTION

		if cmd == 'help':		# Display user help
			reply = USER_HELP_MSG

		elif cmd == 'about':
			reply = ABOUT_MSG
		
		elif cmd == 'add':
			reply = '/add function is disabled at the moment'

		elif cmd == 'invite':	# Let the bot subscribe to another email address
			if len(command) != 2:
				reply = INVITE_FORMAT_MSG % senderJID

			elif senderJID in globalBotIDs:		# Forwarded user subscription from another bot
				guest = command[1]
				guestNode, guestDomain = guest.split('@')
				self.roster.Subscribe(xmpp.protocol.JID(node=guestNode, domain=guestDomain))
				print 'EVENT <%s>: subscribe from %s forwarded by %s' % (self.JID, guest, senderJID)
				return

			else:	# Manual subscription
				guest = command[1]
				if re.match('\S+@\S+\.\w+', guest):	# Check for correct email address format
					if guest in self.roster.getItems():
						reply = '%s is already in the user list.' % guest
					else:
						print 'REPORT (%s): least populated account is %s' % (self.JID, get_LeastPopulatedAccount())
						minNodeJID = get_LeastPopulatedAccount()
						if minNodeJID == self.JID:
							guestNode, guestDomain = guest.split('@')
							self.roster.Subscribe(xmpp.protocol.JID(node=guestNode, domain=guestDomain))
						else:
							# Forward the subscribe request to the least populated bot
							command = '/invite ' + guest
							dispatcher.send(xmpp.Message(minNodeJID, command))
						reply = 'Thank you. %s has been invited to use this system.' % guest
				else:
					reply = '%s is not a valid email address.' % guest

		elif cmd == 'stats':	# Display system statistics
#			roster = self.client.getRoster()
			reply = STATS_MSG % (len(self.roster.getItems()), dictionary.mainDictWC, dictionary.addedDictWC)


		# If the sender is not in the admin list, don't check any further for admin commands
		elif senderJID not in configs['admin']:
			reply = 'invalid command'


# ADMIN COMMANDS SECTION

		# Display administrator help
		elif cmd == 'admin':	
			reply = ADMIN_HELP_MSG

		# Set the status message
		elif cmd == 'status':
			if len(command) > 1:
				presence = xmpp.protocol.Presence()
				presence.setStatus(' '.join(command[1:]))
				dispatcher.send(presence)
				return
			else:
				reply = STATUS_USAGE_MSG

	
		# Broadcast a chat message to all subscribers
		elif cmd == 'broadcast':
			if len(command) > 1:
				message = ' '.join(command[1:])
				for JID in self.roster.getItems():
					dispatcher.send(xmpp.Message(JID, message))
					time.sleep(POLL_INTERVAL)
				return
			else:
				reply = ''
	
		# Delete an existing subscription
		elif cmd == 'delete':
			global contactCounts
			print 'EVENT (%s): delete %s' % (self.JID, senderJID)

			if len(command) == 2:
				targetJID = command[1]
				if targetJID in self.roster.getItems():
					self.roster.Unsubscribe(targetJID)	# If the bot has suscribed, cancel it
					self.roster.Unauthorize(targetJID)	# If the client has subscribed, cancel it
					self.roster.delItem(targetJID)		# Also delete in the roster
					contactCounts[self.JID] -= 1
					reply = '%s has been unsubscribed from the contacts.' % targetJID
				else:
					reply = '%s is not in the current contacts' % targetJID
				
				print 'after delete'
				print self.client.getRoster().getItems()
				print contactCounts


			else:
				reply = 'invalid number of arguments'
			

		# Shutdown the bot completely
		elif cmd == 'exit':
			sys.exit(0)

		else:
			reply = ADMIN_HELP_MSG
	
		dispatcher.send(xmpp.Message(sender, reply))


# END OF DICTIONARY BOT


def get_LeastPopulatedAccount():
	"Get the bot account with the least number of contacts"
	minCount = 10000	# 10K is the subscription limit for Gtalk
	for nodeJID in contactCounts.keys():
		if contactCounts[nodeJID] < minCount:
			minCount = contactCounts[nodeJID]
			minNodeJID = nodeJID
	return minNodeJID


def read_ConfigFile():
	"Read configuration settings for all accounts"
	global configs
	configs = {}
	for line in open(CONFIG_FILE).readlines():
		line = line.strip()
		if len(line) > 0 and not line.startswith('#'):	# Not a blank line or a comment
			key, value = line.split(':')
			key = key.strip().lower()
			value = value.strip()
			if configs.has_key(key):
				configs[key].append(value);
			else:
				configs[key] = [value]


def start_DictBots():
	global botList, nodeCount, globalBotIDs
	botList = []
	globalBotIDs = []
	nodeCount = len(configs['username'])
	for nodeNum in range(0, nodeCount):
		domain = configs['domain'][nodeNum]
		username = configs['username'][nodeNum]
		password = configs['password'][nodeNum]
		url = configs['url'][nodeNum]
		port = int(configs['port'][nodeNum])
		
		print 'ACTION: connecting %s@%s to server' % (username, domain)
		bot = DictBot(domain, username, password, url, port)
		botList.append(bot)
		globalBotIDs.append('%s@%s' % (username, domain))
		print 'EVENT: connected %s to server' % (bot.JID)


def save_ContactLists():
	global contactCounts
	contactCounts = {}	# Maintains the contact counts for each bot in global network
	for bot in botList:
		contactList = bot.roster.getItems()
		contactCounts[bot.JID] = len(contactList)
		filePath = os.path.join(CONTACTS_DIR, 'contacts_%s_%s_%s.txt' % (bot.JID,time.strftime('%Y%m%d%H%M'), len(contactList)))
		contactsFile = open(filePath, 'w')
		contactsFile.write('\n'.join(contactList))
		contactsFile.close()


if __name__ == '__main__':
	dictionary.load_MainDict()
	dictionary.load_AddedDict()
	read_ConfigFile()
	start_DictBots()
	save_ContactLists()
	while True:
		for bot in botList:
			bot.process_Events()
